﻿namespace Webinator.Util
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;

    /// <summary>
    /// Bitmap parsing class.
    /// </summary>
    public class BitmapParser
    {
        /// <summary>
        /// Gets a screenshot of the specified Locator of the window with the specified handle.
        /// </summary>
        /// <param name="handle">The handle.</param>
        /// <param name="locator">The locator.</param>
        /// <param name="webManager">The <c>IWebManager</c> instance.</param>
        /// <returns>
        /// A <c>Bitmap</c>.
        /// </returns>
        public Bitmap GetScreenshotOfLocator(IntPtr handle, Locator locator, IWebManager webManager)
        {
            var locatorPosition = webManager.GetLocatorLocation(locator);
            var webPosition = WindowHelper.GetWindowPosition(webManager.Process.MainWindowHandle);
            var srcRect = new Rectangle(webPosition.X + webManager.ViewportCoordinates.X + locatorPosition.X, webPosition.Y + webManager.ViewportCoordinates.Y + locatorPosition.Y, locatorPosition.Width, locatorPosition.Height);
            if (srcRect.Width > 0 && srcRect.Height > 0)
            {
                Bitmap bmp = new Bitmap(srcRect.Width, srcRect.Height);
                Graphics screenG = Graphics.FromImage(bmp);

                try
                {
                    WindowHelper.SetActiveWindow(handle);
                    screenG.CopyFromScreen(srcRect.Left, srcRect.Top, 0, 0, new Size(srcRect.Width, srcRect.Height), CopyPixelOperation.SourceCopy);
                    return bmp;
                }
                catch
                {
                    return null;
                }
            }

            return null;
        }

        /// <summary>
        /// Gets a screenshot of the window with the specified handle.
        /// </summary>
        /// <param name="handle">The handle.</param>
        /// <returns>
        /// A <c>Bitmap</c>.
        /// </returns>
        public Bitmap GetScreenshotOfWindow(IntPtr handle)
        {
            // Retry 3 times
            Bitmap bmp = null;
            do
            {
                WindowHelper.SetActiveWindow(handle);
                var srcRect = WindowHelper.GetWindowPosition(handle);
                if (srcRect.Width > 0 && srcRect.Height > 0)
                {
                    bmp = new Bitmap(srcRect.Width, srcRect.Height);
                    Graphics screenG = Graphics.FromImage(bmp);

                    try
                    {
                        screenG.CopyFromScreen(srcRect.Left, srcRect.Top, 0, 0, new Size(srcRect.Width, srcRect.Height), CopyPixelOperation.SourceCopy);
                    }
                    catch
                    {
                    }
                }
            } 
            while (bmp == null);

            return bmp;
        }

        /// <summary>
        /// Gets a screenshot of the viewport of the window with the specified handle.
        /// </summary>
        /// <param name="handle">The handle.</param>
        /// <param name="webManager">The <c>IWebManager</c> instance.</param>
        /// <returns>
        /// A <c>Bitmap</c>.
        /// </returns>
        public Bitmap GetScreenshotOfViewport(IntPtr handle, IWebManager webManager)
        {
            var webPosition = WindowHelper.GetWindowPosition(webManager.Process.MainWindowHandle);
            var srcRect = new Rectangle(webManager.ViewportCoordinates.X + webPosition.X, webManager.ViewportCoordinates.Y + webPosition.Y, webManager.ViewportDimensions.Width, webManager.ViewportDimensions.Width);
            if (srcRect.Width > 0 && srcRect.Height > 0)
            {
                Bitmap bmp = new Bitmap(srcRect.Width, srcRect.Height);
                Graphics screenG = Graphics.FromImage(bmp);

                try
                {
                    WindowHelper.SetActiveWindow(handle);
                    screenG.CopyFromScreen(srcRect.Left, srcRect.Top, 0, 0, new Size(srcRect.Width, srcRect.Height), CopyPixelOperation.SourceCopy);
                    return bmp;
                }
                catch
                {
                    return null;
                }
            }

            return null;
        }

        /// <summary>
        /// Finds the location of the sub image within the source image.
        /// </summary>
        /// <param name="sourceBitmap">The source bitmap.</param>
        /// <param name="subBitmap">The sub bitmap.</param>
        /// <returns>
        /// The starting point of the sub image within the source image.
        /// </returns>
        public Point FindSubImage(Bitmap sourceBitmap, Bitmap subBitmap)
        {
            var unsafeBitmap = new UnsafeBitmap(sourceBitmap);
            var unsafeSubBitmap = new UnsafeBitmap(subBitmap);
            unsafeBitmap.LockBitmap();
            unsafeSubBitmap.LockBitmap();
            Point pointMatch = new Point();

            int subWidth = subBitmap.Width;
            int subHeight = subBitmap.Height;
            int width = sourceBitmap.Width - subWidth;
            int height = sourceBitmap.Height - subHeight;

            ////Console.WriteLine(string.Format("W: {0} H: {1} sW: {2} sH: {3}", width, height, subWidth, subHeight));

            foreach (Point point in DiagonalIterator(width, height))
            {
                bool detected = IsImageMatch(point, unsafeBitmap, unsafeSubBitmap, subWidth, subHeight);
                if (detected)
                {
                    pointMatch = point;
                    break;
                }
            }

            unsafeBitmap.UnlockBitmap();
            unsafeSubBitmap.UnlockBitmap();
            unsafeBitmap.Dispose();
            unsafeSubBitmap.Dispose();
            sourceBitmap.Dispose();
            subBitmap.Dispose();

            return pointMatch;
        }

        public Rectangle GetBoundsOfRectangle(Bitmap bitmap, Color color, Size sizeAtLeast, int tolerance)
        {
            // Finds by locating pixels of a given color, then by following subsequent matching pixels
            // in a rectangular pattern until a rectangle is found of the correct size
            var source = new UnsafeBitmap(bitmap);
            source.LockBitmap();
            Rectangle match = new Rectangle();
            int width = bitmap.Width - (sizeAtLeast.Width + 1);
            int height = bitmap.Height - (sizeAtLeast.Height + 1);

            // Check every pixel in vertical lines every pattern width - 1
            foreach (Point point in TopDownIterator(sizeAtLeast.Width - 1, 0, width, height, sizeAtLeast.Width - 1, 1))
            {
                var pixel = source.GetPixel(point.X, point.Y);
                if (WithinRange(pixel, color, tolerance))
                {
                    Rectangle bounds = FindBounds(source, bitmap, color, sizeAtLeast, point, tolerance);
                    if (bounds.Width >= sizeAtLeast.Width && bounds.Height >= sizeAtLeast.Height)
                    {
                        match = bounds;
                        break;
                    }
                }
            }

            source.UnlockBitmap();
            source.Dispose();

            return match;
        }

        /// <summary>
        /// Searches the specified image for the viewport x and y, starts in the upper left and moves toward the bottom right.
        /// </summary>
        /// <param name="bitmap">The bitmap.</param>
        /// <param name="pattern">The color pattern to search for</param>
        /// <returns>A <c>Point</c> indicating the x and y coordinates of the viewport within the image.</returns>
        public Point GetXyOfPattern(Bitmap bitmap, Color[] pattern)
        {
            var screenshot = new UnsafeBitmap(bitmap);
            screenshot.LockBitmap();
            Point viewport = new Point();
            int width = bitmap.Width - (pattern.Length + 1);
            int height = bitmap.Height - 1;

            foreach (Point point in DiagonalIterator(width, height))
            {
                bool detected = IsPatternMatch(point, screenshot, pattern);
                if (detected)
                {
                    viewport = point;
                    break;
                }
            }

            screenshot.UnlockBitmap();
            screenshot.Dispose();
            bitmap.Dispose();

            return viewport;
        }

        /// <summary>
        /// Iterates through points in a diagonal direction starting at the upper left.
        /// </summary>
        /// <param name="maxX">The max x.</param>
        /// <param name="maxY">The max y.</param>
        /// <returns>
        /// An <c>Iterator</c>.
        /// </returns>
        private static IEnumerable<Point> DiagonalIterator(int maxX, int maxY)
        {
            int width = maxX;
            int height = maxY;
            int largerAxis = width > height ? width : height;

            for (int diagonalPoint = 0; diagonalPoint <= largerAxis; diagonalPoint++)
            {
                int x = diagonalPoint > width ? width : diagonalPoint;
                int y = diagonalPoint > height ? height : diagonalPoint;
                yield return new Point(x, y);

                // horizontal line...
                if (diagonalPoint <= height)
                {
                    for (int offset = x - 1; offset >= 0; offset--)
                    {
                        yield return new Point(offset, diagonalPoint);
                    }
                }

                // vertical line...
                if (diagonalPoint <= width)
                {
                    for (int offset = y - 1; offset >= 0; offset--)
                    {
                        yield return new Point(diagonalPoint, offset);
                    }
                }
            }
        }

        /// <summary>
        /// Iterates through points in a top down direction starting at the upper left.
        /// </summary>
        /// <param name="minX">The min x.</param>
        /// <param name="minY">The min y.</param>
        /// <param name="maxX">The max x.</param>
        /// <param name="maxY">The max y.</param>
        /// <param name="stepX">The y step.</param>
        /// <param name="stepY">The x step.</param>
        /// <returns>
        /// An <c>Iterator</c>.
        /// </returns>
        private static IEnumerable<Point> TopDownIterator(int minX, int minY, int maxX, int maxY, int stepX, int stepY)
        {
            for (int x = minX; x < maxX; x += stepX)
            {
                for (int y = minY; y < maxY; y += stepY)
                {
                    yield return new Point(x, y);
                }
            }
        }

        /// <summary>
        /// Determines if a pixel pattern match is found at the specified point.
        /// </summary>
        /// <param name="point">The point.</param>
        /// <param name="unsafeBitmap">The unsafe bitmap.</param>
        /// <param name="pattern">The pattern.</param>
        /// <returns>
        /// True if the pattern is found, false if it isn't.
        /// </returns>
        private bool IsPatternMatch(Point point, UnsafeBitmap unsafeBitmap, Color[] pattern)
        {
            bool detected = false;
            for (int i = 0; i < 20; ++i)
            {
                PixelData pixel = unsafeBitmap.GetPixel(point.X + i, point.Y);
                var color = pattern[i];
                if (pixel.blue == color.B && pixel.green == color.G && pixel.red == color.R)
                {
                    detected = true;
                }
                else
                {
                    detected = false;
                    break;
                }
            }

            return detected;
        }

        private Rectangle FindBounds(UnsafeBitmap bitmap, Bitmap source, Color color, Size sizeAtLeast, Point point, int tolerance)
        {
            int width = 0;
            int height = 0;

            // Go left until no match
            for (int x = point.X - 1; x >= 0; x--)
            {
                var pixel = bitmap.GetPixel(x, point.Y);
                if (!WithinRange(pixel, color, tolerance))
                {
                    point.X = x + 1;
                    break;
                }
            }

            // Go right until no match
            for (int x = point.X + 1; x < source.Width; x++)
            {
                var pixel = bitmap.GetPixel(x, point.Y);
                if (!WithinRange(pixel, color, tolerance))
                {
                    width = x - 1;
                    break;
                }
            }

            if (width - point.X + 1 < sizeAtLeast.Width)
            {
                return new Rectangle(0, 0, 0, 0);
            }

            // Go down until no match
            for (int y = point.Y + 1; y < source.Height; y++)
            {
                var pixel = bitmap.GetPixel(width, y);
                if (!WithinRange(pixel, color, tolerance))
                {
                    height = y - 1;
                    break;
                }
            }

            if (height - point.Y + 1 < sizeAtLeast.Height)
            {
                return new Rectangle(0, 0, 0, 0);
            }

            // Finish matching the rectangle
            for (int x = width - 1; x > point.X; x--)
            {
                var pixel = bitmap.GetPixel(x, height);
                if (!WithinRange(pixel, color, tolerance))
                {
                    return new Rectangle(0, 0, 0, 0);
                }
            }

            for (int y = height; y > point.Y; y--)
            {
                var pixel = bitmap.GetPixel(point.X, y);
                if (!WithinRange(pixel, color, tolerance))
                {
                    return new Rectangle(0, 0, 0, 0);
                }
            }

            return new Rectangle(point.X, point.Y, width - point.X + 1, height - point.Y + 1);
        }

        private bool IsImageMatch(Point point, UnsafeBitmap unsafeBitmap, UnsafeBitmap unsafeSubBitmap, int subWidth, int subHeight)
        {
            bool detected = false;

            foreach (var pt in TopDownIterator(0, 0, subWidth, subHeight, 1, 1))
            {
                PixelData pixelOne = unsafeBitmap.GetPixel(pt.X + point.X, pt.Y + point.Y);
                PixelData pixelTwo = unsafeSubBitmap.GetPixel(pt.X, pt.Y);
                detected = IsPixelMatch(pixelOne, pixelTwo);
                if (!detected) break;
            }

            return detected;
        }

        private bool IsPixelMatch(PixelData pixelOne, PixelData pixelTwo)
        {
            return pixelOne.blue == pixelTwo.blue && pixelOne.red == pixelTwo.red && pixelOne.green == pixelTwo.green;
        }

        /// <summary>
        /// Determines if two colors are close to each other.
        /// </summary>
        /// <param name="pixel">The first color.</param>
        /// <param name="color">The second color.</param>
        /// <param name="tolerance">The tolerance by percentage.</param>
        /// <returns>
        /// True if the pixels are closely related as defined by the tolerance.
        /// False if they are not.
        /// </returns>
        public bool WithinRange(PixelData pixel, Color color, int tolerance)
        {
            return ColorComparer.ColorDifferenceLab(color.R, color.G, color.B, pixel.red, pixel.green, pixel.blue) <= tolerance;
        }
    }
}
